/*
 * @(#)DefaultOverlayable.java 7/31/2007
 *
 * Copyright 2002 - 2007 JIDE Software Inc. All rights reserved.
 */

package com.element.swing.overlay;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Vector;

/**
 * DefaultOverlayable是使用 JPanel 作为基础组件的Overlayable的默认实现。
 */
public class DefaultOverlayable extends JPanel implements Overlayable, ComponentListener {
	private JComponent _actualComponent;
	private Insets _overlayLocationInsets = new Insets(0, 0, 0, 0);
	private List<JComponent> _overlayComponents;
	private Map<JComponent, Integer> _overlayLocations;

	public DefaultOverlayable() {
		initComponents();
	}

	public DefaultOverlayable(JComponent component) {
		initComponents();
		setActualComponent(component);
	}

	public DefaultOverlayable(JComponent actualComponent, JComponent overlayComponent, int overlayLocation) {
		initComponents();
		setActualComponent(actualComponent);
		addOverlayComponent(overlayComponent, overlayLocation);
	}

	public DefaultOverlayable(JComponent actualComponent, JComponent overlayComponent) {
		initComponents();
		setActualComponent(actualComponent);
		addOverlayComponent(overlayComponent, SwingConstants.CENTER);
	}

	private void initComponents() {
		setLayout(null);
		_overlayComponents = new Vector<>();
		_overlayLocations = new HashMap<>();
	}

	/**
	 * Override to consider the overlayLocationInsets. If the overlayLocationInsets's edges are positive number, we will
	 * increase the preferred size so that the overlayo component can be shown. If they are negative, we will still keep
	 * the super.getPreferredSize.
	 *
	 * @return the preferred size of the DefaultOverlayable.
	 */
	@Override
	public Dimension getPreferredSize() {
		Dimension size = getActualComponent() == null ? new Dimension(0, 0) : getActualComponent().getPreferredSize();
		Insets insets = getOverlayLocationInsets();
		if (insets != null) {
			size.width += Math.max(0, insets.left) + Math.max(0, insets.right);
			size.height += Math.max(0, insets.top) + Math.max(0, insets.bottom);
		}
		return size;
	}

	@Override
	public void setPreferredSize(Dimension preferredSize) {
		super.setPreferredSize(preferredSize);
		if (getActualComponent() != null) {
			if (preferredSize != null) {
				Insets insets = getOverlayLocationInsets();
				if (insets != null) {
					preferredSize.width -= Math.max(0, insets.left) + Math.max(0, insets.right);
					preferredSize.width = Math.max(0, preferredSize.width);
					preferredSize.height -= Math.max(0, insets.top) + Math.max(0, insets.bottom);
					preferredSize.height = Math.max(0, preferredSize.height);
				}
			}
			getActualComponent().setPreferredSize(preferredSize);
		}
	}

	@Override
	public Dimension getMinimumSize() {
		Dimension size = getActualComponent() == null ? new Dimension(0, 0) : getActualComponent().getMinimumSize();
		Insets insets = getOverlayLocationInsets();
		if (insets != null) {
			size.width += Math.max(0, insets.left) + Math.max(0, insets.right);
			size.height += Math.max(0, insets.top) + Math.max(0, insets.bottom);
		}
		return size;
	}

	@Override
	public void setMinimumSize(Dimension minimumSize) {
		super.setMinimumSize(minimumSize);
		if (getActualComponent() != null) {
			if (minimumSize != null) {
				Insets insets = getOverlayLocationInsets();
				if (insets != null) {
					minimumSize.width -= Math.max(0, insets.left) + Math.max(0, insets.right);
					minimumSize.width = Math.max(0, minimumSize.width);
					minimumSize.height -= Math.max(0, insets.top) + Math.max(0, insets.bottom);
					minimumSize.height = Math.max(0, minimumSize.height);
				}
			}
			getActualComponent().setMinimumSize(minimumSize);
		}
	}

	@Override
	public void setBounds(int x, int y, int width, int height) {
		super.setBounds(x, y, width, height);

		Insets insets = getOverlayLocationInsets();
		if (insets != null) {
			x = Math.max(0, insets.left);
			y = Math.max(0, insets.top);
			width -= Math.max(0, insets.left) + Math.max(0, insets.right);
			height -= Math.max(0, insets.top) + Math.max(0, insets.bottom);
		}
		if (getActualComponent() != null) {
			getActualComponent().setBounds(x, y, width, height);
		}

		updateLocation();
	}

	private void updateLocation() {
		JComponent[] components = getOverlayComponents();
		for (JComponent c : components) {
			if (c == null) {
				return;
			}

			Rectangle r = getOverlayComponentBounds(c);
			c.setBounds(r);
		}
	}

	/**
	 * Gets the bounds of an overlay component.
	 *
	 * @param component an overlay component
	 * @return the bounds of the overlay component.
	 */
	protected Rectangle getOverlayComponentBounds(JComponent component) {
		Component relativeComponent = getActualComponent();

		Rectangle bounds = relativeComponent.getBounds();
		if (relativeComponent != getActualComponent()) {
			bounds = SwingUtilities.convertRectangle(relativeComponent.getParent(), bounds, getActualComponent());
		}
		Rectangle overlayBounds = new Rectangle(bounds);
		Insets insets = getOverlayLocationInsets();
		overlayBounds.x -= insets.left;
		overlayBounds.y -= insets.top;
		overlayBounds.width += insets.left + insets.right;
		overlayBounds.height += insets.top + insets.bottom;

		int cx = 0;
		int cy = 0;

		Dimension size = component.getPreferredSize();
		int cw = size.width;
		int ch = size.height;

		int overlayLocation = getOverlayLocation(component);

		// adjust the location based on the orientation
		if (overlayLocation == LEADING) {
			if (component.getComponentOrientation().isLeftToRight()) {
				overlayLocation = WEST;
			} else {
				overlayLocation = EAST;
			}
		} else if (overlayLocation == TRAILING) {
			if (component.getComponentOrientation().isLeftToRight()) {
				overlayLocation = EAST;
			} else {
				overlayLocation = WEST;
			}
		}

		switch (overlayLocation) {
			case CENTER -> {
				cx = bounds.x + (bounds.width - cw) / 2;
				cy = bounds.y + (bounds.height - ch) / 2;
			}
			case NORTH -> {
				cx = bounds.x + (bounds.width - cw) / 2;
				cy = overlayBounds.y;
			}
			case SOUTH -> {
				cx = bounds.x + (bounds.width - cw) / 2;
				cy = overlayBounds.y + overlayBounds.height - ch;
			}
			case WEST -> {
				cx = overlayBounds.x;
				cy = bounds.y + (bounds.height - ch) / 2;
			}
			case EAST -> {
				cx = overlayBounds.x + overlayBounds.width - cw;
				cy = bounds.y + (bounds.height - ch) / 2;
			}
			case NORTH_WEST -> {
				cx = overlayBounds.x;
				cy = overlayBounds.y;
			}
			case NORTH_EAST -> {
				cx = overlayBounds.x + overlayBounds.width - cw;
				cy = overlayBounds.y;
			}
			case SOUTH_WEST -> {
				cx = overlayBounds.x;
				cy = overlayBounds.y + overlayBounds.height - ch;
			}
			case SOUTH_EAST -> {
				cx = overlayBounds.x + overlayBounds.width - cw;
				cy = overlayBounds.y + overlayBounds.height - ch;
			}
		}

		return new Rectangle(cx, cy, cw, ch);
	}

	public int getOverlayLocation(JComponent component) {
		Integer location = _overlayLocations.get(component);
		if (location != null) {
			return location;
		} else {
			return -1;
		}
	}

	public void setOverlayLocation(JComponent component, int location) {
		setOverlayLocation(component, null, location);
	}

	@SuppressWarnings({"UnusedDeclaration"})
	private void setOverlayLocation(JComponent component, Component relativeComponent, int location) {
		boolean updated = false;
		int old = getOverlayLocation(component);
		if (old != location) {
			_overlayLocations.put(component, location);
			updated = true;
		}
		if (updated) {
			updateLocation();
		}
	}

	public void addOverlayComponent(JComponent component) {
		addOverlayComponent(component, SwingConstants.CENTER, -1);
	}

	public void addOverlayComponent(JComponent component, int location) {
		addOverlayComponent(component, location, -1);
	}

	public void addOverlayComponent(JComponent component, int location, int index) {
		addOverlayComponent(component, null, location, index);
	}

	private void addOverlayComponent(JComponent component, Component relativeComponent, int location, int index) {
		_overlayComponents.remove(component);
		if (index == -1) {
			_overlayComponents.add(component);
			add(component, getComponentCount() - 1); // add it before the the actual component
		} else {
			_overlayComponents.add(index, component);
			add(component, index);
		}
		setOverlayLocation(component, relativeComponent, location);
	}

	public void removeOverlayComponent(JComponent component) {
		if (_overlayComponents.contains(component)) {
			_overlayComponents.remove(component);
			_overlayLocations.remove(component);
			remove(component);
		}
	}

	public JComponent[] getOverlayComponents() {
		return _overlayComponents.toArray(new JComponent[0]);
	}

	public JComponent getActualComponent() {
		return _actualComponent;
	}

	public void setActualComponent(JComponent actualComponent) {
		if (_actualComponent != null) {
			remove(_actualComponent);
			_actualComponent.putClientProperty(CLIENT_PROPERTY_OVERLAYABLE, null);
		}
		_actualComponent = actualComponent;
		_actualComponent.putClientProperty(CLIENT_PROPERTY_OVERLAYABLE, this);
		add(_actualComponent);
		Container container = getParent();
		if (container != null) {
			invalidate();
			container.validate();
		}
	}

	public Insets getOverlayLocationInsets() {
		return _overlayLocationInsets;
	}

	public void setOverlayLocationInsets(Insets overlayLocationInsets) {
		_overlayLocationInsets = overlayLocationInsets;
		Container container = getParent();
		if (container != null) {
			invalidate();
			container.validate();
		}
	}

	public void setOverlayVisible(boolean visible) {
		JComponent[] components = getOverlayComponents();
		for (JComponent component : components) {
			component.setVisible(visible);
		}
	}

	public void componentResized(ComponentEvent e) {
		updateLocation();
	}

	public void componentMoved(ComponentEvent e) {
		updateLocation();
	}

	public void componentShown(ComponentEvent e) {
		updateLocation();
	}

	public void componentHidden(ComponentEvent e) {
		updateLocation();
	}
}
